﻿// Copyright (c) 2013 fancidev
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the "Software"),
// to deal in the Software without restriction, including without limitation
// the rights to use, copy, modify, merge, publish, distribute, sublicense,
// and/or sell copies of the Software, and to permit persons to whom the
// Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in
// all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
// THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
// FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.

// Tags.cs -- defines SWF tags.

using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Drawing;
using System.Drawing.Drawing2D;
using System.Drawing.Imaging;
using System.IO;
using System.IO.Compression;
//using System.Linq;
using System.Runtime.InteropServices;
//using System.Text;
using Util.IO;
using Util.IO.Compression;

namespace Util.Media.Flash
{
    [TypeConverter(typeof(ExpandableObjectConverter))]
    public abstract class Tag
    {
        /// <summary>
        /// Gets the type of the tag.
        /// </summary>
        /// <remarks>
        /// We make this an abstract property instead of using a private 
        /// field to store it because the user may create a specific tag
        /// directly using <code>new XyzTag()</code>, and in that case
        /// any private field has to be set explicitly.
        /// </remarks>
        [Browsable(false)]
        public abstract TagType TagType { get; }

        /// <summary>
        /// Gets the length in bytes of the tag's data when serialized to a
        /// stream, excluding the type and length header. If this object was
        /// not deserialized from a stream, returns 0.
        /// </summary>
        [Browsable(false)]
        public int TagLength { get; private set; }

        /// <summary>
        /// When overridden in a derived class, deserializes the tag's data
        /// from the supplied stream. The stream position is set just after
        /// the tag type and length header, and the stream length is set to
        /// the tag length. Therefore, the derived class should consider the
        /// entire stream to contain the tag's data. 
        /// 
        /// The stream may not be seekable.
        /// </summary>
        protected abstract void DeserializeCore(FlashReader reader);

        /// <summary>
        /// When overridden in a derived class, serializes the tag's data to
        /// the given stream. Only the tag's data should be serialized; the 
        /// tag type and length header should not be serialzied. The tag's
        /// length is inferred from the number of bytes written to the stream.
        /// </summary>
        protected abstract void SerializeCore(FlashWriter writer);

        /// <summary>
        /// Deserializes a tag from the supplied stream. The type of the 
        /// returned object will be either XyzTag or UnknownTag.
        /// </summary>
        /// <returns></returns>
        public static Tag Deserialize(FlashReader reader)
        {
            // Read RECORDHEADER.
            ushort tagCodeAndLength = reader.ReadUInt16();
            TagType tagType = (TagType)(tagCodeAndLength >> 6);
            int tagLength = tagCodeAndLength & 0x3F;
            if (tagLength == 63)
            {
                tagLength = reader.ReadInt32();
                if (tagLength < 0)
                    throw new InvalidDataException("TagLength must be non-negative.");
            }

            // Create an instance of XYZTag according to the tag code.
            Tag tag;
            switch (tagType)
            {
                case TagType.End:
                    tag = new EndTag();
                    break;
                case TagType.ShowFrame:
                    tag = new ShowFrameTag();
                    break;
                case TagType.SetBackgroundColor:
                    tag = new SetBackgroundColorTag();
                    break;
                case TagType.PlaceObject2:
                    tag = new PlaceObject2Tag();
                    break;
                case TagType.RemoveObject2:
                    tag = new RemoveObject2Tag();
                    break;
                case TagType.DefineShape3:
                    tag = new DefineShape3Tag();
                    break;
                case TagType.DefineBitsLossless2:
                    tag = new DefineBitsLossless2Tag();
                    break;
                default:
                    tag = new UnknownTag(tagType);
                    break;
            }
            tag.TagLength = tagLength;

            // Create a stream view with length set to the tag length.
            // This prevents the Tag class from reading past its end,
            // and simplifies the deserializer as it can do whatever
            // with the stream.
            using (LimitedLengthStream tagStream = new LimitedLengthStream(
                   reader.BaseStream, tagLength, true))
            using (FlashReader tagReader = new FlashReader(tagStream))
            {
                tag.DeserializeCore(tagReader);

                // Discard the remaining bytes in the tag's packet.
                long numDiscard = tagStream.Length - tagStream.Position;
                if (numDiscard > 0)
                {
                    System.Diagnostics.Debug.WriteLine(
                        "Discarded {0} bytes in tag {1}.",
                        numDiscard, tagType);
                    reader.BaseStream.Skip(numDiscard);
                }
            }
            return tag;
        }

        /// <summary>
        /// Serializes this tag to a stream. This method writes the tag type
        /// and length header, followed by the actual tag data specific to
        /// the particular type of tag represented by this object.
        /// </summary>
        public void Serialize(FlashWriter writer)
        {
            // Serialize the actual content of the tag (excluding the header)
            // to a memory stream, so that we can find out its length. This 
            // works if the underlying stream is not seekable.
            using (MemoryStream tagStream = new MemoryStream())
            using (FlashWriter tagWriter = new FlashWriter(tagStream))
            {
                this.SerializeCore(tagWriter);
                
                // TODO: what if SerializeCore closes the tagStream?

                long tagLength = tagStream.Length;
                if (tagLength > Int32.MaxValue)
                    throw new ArgumentException("The tag stream is too long to encode.");

                ushort tagCodeAndLength = (ushort)((int)TagType << 6);
                if (tagLength < 63)
                {
                    tagCodeAndLength |= (ushort)tagLength;
                    writer.Write(tagCodeAndLength);
                }
                else
                {
                    tagCodeAndLength |= 0x3F;
                    writer.Write(tagCodeAndLength);
                    writer.Write((int)tagLength);
                }

                writer.Write(tagStream.GetBuffer(), 0, (int)tagLength);
            }
        }

        public override string ToString()
        {
            return TagType.ToString();
        }
    }

    public class UnknownTag : Tag
    {
        private TagType tagType;

        public byte[] Data { get; set; }

        public UnknownTag(TagType type)
        {
            this.tagType = type;
        }

        protected override void DeserializeCore(FlashReader reader)
        {
            this.Data = new byte[TagLength];
            reader.ReadFull(this.Data, 0, this.Data.Length);
        }

        protected override void SerializeCore(FlashWriter writer)
        {
            writer.Write(this.Data);
        }

        public override string ToString()
        {
            return string.Format("? {0} ({1})", this.TagType, this.TagLength);
        }

        public override TagType TagType
        {
            get { return this.tagType; }
        }
    }

    public class EndTag : Tag
    {
        public override TagType TagType { get { return TagType.End; } }
        protected override void DeserializeCore(FlashReader reader) { }
        protected override void SerializeCore(FlashWriter writer) { }
    }

    public class ShowFrameTag : Tag
    {
        public override TagType TagType { get { return TagType.ShowFrame; } }
        protected override void DeserializeCore(FlashReader reader) { }
        protected override void SerializeCore(FlashWriter writer) { }
    }

    public class SetBackgroundColorTag : Tag
    {
        private Color color = Color.Black;

        /// <summary>
        /// Gets or sets the background color. The color must be opaque.
        /// </summary>
        /// <exception cref="ArgumentException">The new color is not opaque,
        /// i.e. it has an alpha value less than 255.
        /// </exception>
        public Color Color
        {
            get { return color; }
            set
            {
                if (value.A != 255)
                    throw new ArgumentException("The background color must be opaque.");
                this.color = value;
            }
        }

        public override TagType TagType
        {
            get { return TagType.SetBackgroundColor; }
        }

        protected override void DeserializeCore(FlashReader reader)
        {
            this.Color = reader.ReadRGB();
        }

        protected override void SerializeCore(FlashWriter writer)
        {
            writer.WriteRGB(this.Color);
        }
    }

    public class DefineBitsLossless2Tag : Tag
    {
#if false
        private ushort characterId;
        private BitmapFormat bitmapFormat; // byte
        private ushort bitmapWidth;
        private ushort bitmapHeight;
#endif
        public override TagType TagType { get { return TagType.DefineBitsLossless2; } }

        public ushort CharacterId { get; set; }

        [Browsable(false)]
        protected BitmapFormat BitmapFormat { get; set; }

        [Description("Pixel format of the bitmap.")]
        public PixelFormat PixelFormat
        {
            get
            {
                if (BitmapFormat == BitmapFormat.Indexed)
                    return PixelFormat.Format8bppIndexed;
                else if (BitmapFormat == BitmapFormat.TrueColor)
                    return PixelFormat.Format32bppPArgb;
                else
                    return PixelFormat.Undefined;
            }
            set
            {
                if (value == PixelFormat.Format8bppIndexed)
                    BitmapFormat = BitmapFormat.Indexed;
                else if (value == PixelFormat.Format32bppPArgb)
                    BitmapFormat = BitmapFormat.TrueColor;
                else
                    throw new ArgumentException("Unsupported pixel format.");
            }
        }

        [Description("Bitmap width in pixels.")]
        public ushort BitmapWidth { get; set; }

        [Description("Bitmap height in pixels.")]
        public ushort BitmapHeight { get; set; }

        [Description("Number of entries in the palette.")]
        public ushort PaletteSize { get; set; }

        [Description("Palette entries.")]
        public Color[] Palette { get; set; }

        [Description("Raw pixel data. Scanlines are DWORD aligned.")]
        public byte[] BitmapBuffer { get; set; }

        protected override void DeserializeCore(FlashReader reader)
        {
            this.CharacterId = reader.ReadUInt16();
            this.BitmapFormat = (BitmapFormat)reader.ReadByte();
            this.BitmapWidth = reader.ReadUInt16();
            this.BitmapHeight = reader.ReadUInt16();
            if (BitmapFormat == BitmapFormat.Indexed)
                this.PaletteSize = (ushort)(1 + reader.ReadByte());
            else
                this.PaletteSize = 0;

            using (ZLibStream zstream =
                   new ZLibStream(reader.BaseStream, CompressionMode.Decompress))
            using (FlashReader bmpReader = new FlashReader(zstream))
            {
                if (BitmapFormat == BitmapFormat.Indexed)
                {
                    this.Palette = new Color[PaletteSize];
                    for (int i = 0; i < PaletteSize; i++)
                        Palette[i] = bmpReader.ReadRGBA();

                    int stride = (BitmapWidth + 3) / 4 * 4;
                    byte[] pixels = new byte[stride * BitmapHeight];
                    bmpReader.ReadFull(pixels, 0, pixels.Length);
                    this.BitmapBuffer = pixels;
                }
                else if (BitmapFormat == BitmapFormat.TrueColor) // 32-bpp PArgb
                {
                    int n = BitmapWidth * BitmapHeight;
                    byte[] argb = new byte[4 * n];
                    bmpReader.ReadFull(argb, 0, argb.Length);

                    // Reverse the pixel byte order below, because the pixels
                    // in SWF are stored as A,R,G,B in big-endian order, but
                    // the pixels in Windows GDI are stored in little endian
                    // order.
                    for (int i = 0; i < n; i++)
                    {
                        byte t;
                        t = argb[4 * i + 0];
                        argb[4 * i + 0] = argb[4 * i + 3];
                        argb[4 * i + 3] = t;
                        t = argb[4 * i + 1];
                        argb[4 * i + 1] = argb[4 * i + 2];
                        argb[4 * i + 2] = t;
                    }
                    this.BitmapBuffer = argb;
                }
            }
        }

        protected override void SerializeCore(FlashWriter writer)
        {
            writer.Write(this.CharacterId);
            writer.Write((byte)this.BitmapFormat);
            writer.Write(this.BitmapWidth);
            writer.Write(this.BitmapHeight);

            if (BitmapFormat == BitmapFormat.Indexed)
            {
                if (this.Palette.Length == 0)
                    throw new InvalidOperationException("Palette missing.");
                writer.Write((byte)(this.Palette.Length - 1));
                throw new NotImplementedException();
            }
            else // ARGB data with pre-multiplied alpha
            {
                int n = BitmapWidth * BitmapHeight;
                byte[] argb = new byte[4 * n];
                Array.Copy(this.BitmapBuffer, argb, argb.Length);

                // Reverse the pixel byte order below, because the pixels
                // in SWF are stored as A,R,G,B in big-endian order, but
                // the pixels in Windows GDI are stored in little endian
                // order.
                for (int i = 0; i < n; i++)
                {
                    byte t;
                    t = argb[4 * i + 0];
                    argb[4 * i + 0] = argb[4 * i + 3];
                    argb[4 * i + 3] = t;
                    t = argb[4 * i + 1];
                    argb[4 * i + 1] = argb[4 * i + 2];
                    argb[4 * i + 2] = t;
                }

                using (ZLibStream zstream = new ZLibStream(
                    writer.BaseStream, CompressionMode.Compress, true))
                {
                    zstream.Write(argb, 0, argb.Length);
                }
            }
        }

        public void SetBitmap(Bitmap bmp)
        {
            if (bmp.PixelFormat.HasFlag(PixelFormat.Indexed))
                throw new NotImplementedException();

            this.BitmapWidth = (ushort)bmp.Width;
            this.BitmapHeight = (ushort)bmp.Height;
            this.BitmapFormat = Flash.BitmapFormat.TrueColor;
            this.Palette = null;
            this.BitmapBuffer = new byte[bmp.Width * bmp.Height * 4];

            BitmapData bmpData = bmp.LockBits(
                new Rectangle(0, 0, bmp.Width, bmp.Height),
                ImageLockMode.ReadOnly, PixelFormat.Format32bppPArgb);
            Marshal.Copy(bmpData.Scan0, this.BitmapBuffer, 0, this.BitmapBuffer.Length);
            bmp.UnlockBits(bmpData);
        }

        public Bitmap GetBitmap()
        {
            Bitmap bmp = new Bitmap(BitmapWidth, BitmapHeight, PixelFormat);
            BitmapData bmpData = bmp.LockBits(
                new Rectangle(0, 0, bmp.Width, bmp.Height),
                ImageLockMode.WriteOnly, bmp.PixelFormat);
            Marshal.Copy(this.BitmapBuffer, 0, bmpData.Scan0, this.BitmapBuffer.Length);
            bmp.UnlockBits(bmpData);

            if (this.PixelFormat == PixelFormat.Format8bppIndexed)
            {
                ColorPalette p = bmp.Palette;
                Palette.CopyTo(p.Entries, 0);
                bmp.Palette = p;
            }
            return bmp;
        }
    }

    public enum BitmapFormat : byte
    {
        /// <summary>
        /// 8-bpp indexed image. Supported by DefineBitsLossless and
        /// DefineBitsLossless2.
        /// </summary>
        Indexed = 3,

        /// <summary>
        /// 15-bpp RGB image. Only supported by DefineBitsLossless.
        /// </summary>
        HighColor = 4,

        /// <summary>
        /// When used in DefineBitsLossless, specifies 24 bpp RGB image.
        /// When used in DefineBitsLossless2, specifies 32 bpp pre-multiplied
        /// ARGB image.
        /// </summary>
        TrueColor = 5,
    }

    public class PlaceObject2Tag : Tag
    {
        public override TagType TagType { get { return TagType.PlaceObject2; } }

        [DefaultValue(PlaceFlags2.None)]
        public PlaceFlags2 Flags { get; set; }

        public ushort Depth { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort CharacterId { get; set; }

        [TypeConverter(typeof(ExpandableObjectConverter))]
        public Matrix Matrix { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort Ratio { get; set; }
        public string Name { get; set; }

        [DefaultValue(typeof(ushort), "0")]
        public ushort ClipDepth { get; set; }

        protected override void DeserializeCore(FlashReader reader)
        {
            this.Flags = (PlaceFlags2)reader.ReadByte();
            this.Depth = reader.ReadUInt16();

            if (Flags.HasFlag(PlaceFlags2.HasCharacter))
                this.CharacterId = reader.ReadUInt16();
            if (Flags.HasFlag(PlaceFlags2.HasMatrix))
                this.Matrix = reader.ReadMatrix();
            if (Flags.HasFlag(PlaceFlags2.HasColorTransform))
                throw new NotImplementedException();
            if (Flags.HasFlag(PlaceFlags2.HasRatio))
                this.Ratio = reader.ReadUInt16();
            if (Flags.HasFlag(PlaceFlags2.HasName))
                this.Name = reader.ReadString();
            if (Flags.HasFlag(PlaceFlags2.HasClipDepth))
                this.ClipDepth = reader.ReadUInt16();
            if (Flags.HasFlag(PlaceFlags2.HasClipActions))
                throw new NotImplementedException();
        }

        protected override void SerializeCore(FlashWriter writer)
        {
            writer.Write((byte)this.Flags);
            writer.Write((ushort)this.Depth);

            if (Flags.HasFlag(PlaceFlags2.HasCharacter))
                writer.Write((ushort)this.CharacterId);
            if (Flags.HasFlag(PlaceFlags2.HasMatrix))
                writer.WriteMatrix(this.Matrix);
            if (Flags.HasFlag(PlaceFlags2.HasColorTransform))
                throw new NotImplementedException();
            if (Flags.HasFlag(PlaceFlags2.HasRatio))
                writer.Write((ushort)this.Ratio);
            if (Flags.HasFlag(PlaceFlags2.HasName))
                writer.Write(this.Name);
            if (Flags.HasFlag(PlaceFlags2.HasClipDepth))
                writer.Write((ushort)this.ClipDepth);
            if (Flags.HasFlag(PlaceFlags2.HasClipActions))
                throw new NotImplementedException();
        }
    }

    /// <summary>
    /// Flags used by PlaceObject2Tag and PlaceObject3Tag to indicate
    /// which fields are set in the tag.
    /// </summary>
    [Flags]
    public enum PlaceFlags2 : byte
    {
        None = 0,
        Move = 0x01,
        HasCharacter = 0x02,
        HasMatrix = 0x04,
        HasColorTransform = 0x08,
        HasRatio = 0x10,
        HasName = 0x20,
        HasClipDepth = 0x40,
        HasClipActions = 0x80, // SWF 5 and later
    }

    public class RemoveObject2Tag : Tag
    {
        public override TagType TagType { get { return TagType.RemoveObject2; } }

        public ushort Depth { get; set; }

        protected override void DeserializeCore(FlashReader reader)
        {
            this.Depth = reader.ReadUInt16();
        }

        protected override void SerializeCore(FlashWriter writer)
        {
            writer.Write(this.Depth);
        }
    }

    public class DefineShape3Tag : Tag
    {
        public override TagType TagType
        {
            get { return TagType.DefineShape3; }
        }

        public ushort ShapeId { get; set; }
        public RectangleF ShapeBounds { get; set; }
        public FillStyle[] FillStyles { get; set; }
        public LineStyle[] LineStyles { get; set; }
        public ShapeRecord[] ShapeRecords { get; set; }

        protected override void DeserializeCore(FlashReader reader)
        {
            this.ShapeId = reader.ReadUInt16();
            this.ShapeBounds = reader.ReadRectangleF();
            this.FillStyles = FillStyle.ReadObjectArray(reader, 3);
            this.LineStyles = LineStyle.ReadObjectArray(reader, 3);
            this.ShapeRecords = ShapeRecord.DeserializeArray(reader);
        }

        protected override void SerializeCore(FlashWriter writer)
        {
            writer.Write((ushort)this.ShapeId);
            writer.WriteRectangleF(this.ShapeBounds);
            FillStyle.SerializeArray(writer, this.FillStyles, 3);
            LineStyle.SerializeArray(writer, this.LineStyles, 3);
            ShapeRecord.SerializeArray(writer, this.ShapeRecords, 3);
        }
    }

    public enum TagType : ushort
    {
        End = 0,
        ShowFrame = 1,
        DefineShape = 2,
        PlaceObject = 4,
        RemoveObject = 5,
        DefineBits = 6,
        DefineButton = 7,
        JPEGTables = 8,
        SetBackgroundColor = 9,
        DefineFont = 10,
        DefineText = 11,
        DoAction = 12,
        DefineFontInfo = 13,
        DefineSound = 14,
        StartSound = 15,
        DefineButtonSound = 17,
        SoundStreamHead = 18,
        SoundStreamBlock = 19,
        DefineBitsLossless = 20,
        DefineBitsJPEG2 = 21,
        DefineShape2 = 22,
        DefineButtonCxform = 23,
        Protect = 24,
        PlaceObject2 = 26,
        RemoveObject2 = 28,
        DefineShape3 = 32,
        DefineText2 = 33,
        DefineButton2 = 34,
        DefineBitsJPEG3 = 35,
        DefineBitsLossless2 = 36,
        DefineEditText = 37,
        DefineSprite = 39,
        FrameLabel = 43,
        SoundStreamHead2 = 45,
        DefineMorphShape = 46,
        DefineFont2 = 48,
        ExportAssets = 56,
        ImportAssets = 57,
        EnableDebugger = 58,
        DoInitAction = 59,
        DefineVideoStream = 60,
        VideoFrame = 61,
        DefineFontInfo2 = 62,
        EnableDebugger2 = 64,
        ScriptLimits = 65,
        SetTabIndex = 66,
        FileAttributes = 69,
        PlaceObject3 = 70,
        ImportAssets2 = 71,
        DefineFontAlignZones = 73,
        CSMTextSettings = 74,
        DefineFont3 = 75,
        SymbolClass = 76,
        Metadata = 77,
        DefineScalingGrid = 78,
        DoABC = 82,
        DefineShape4 = 83,
        DefineMorphShape2 = 84,
        DefineSceneAndFrameLabelData = 86,
        DefineBinaryData = 87,
        DefineFontName = 88,
        StartSound2 = 89,
        DefineBitsJPEG4 = 90,
        DefineFont4 = 91,
    }
}
